/*
 * Copyright (c) 2018, apexes.net. All rights reserved.
 *
 *         http://www.apexes.net
 *
 */
package net.apexes.commons.lang;

import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import java.security.SecureRandom;
import java.util.Arrays;

/**
 * @author <a href="mailto:hedyn@foxmail.com">HeDYn</a>
 */
public class Passwords {

    private static final PasswordHelper defaultHelper = new PasswordHelper();
    
    /**
     * 生成明文密码的摘要
     * @param password 明文密码
     * @return 返回生成的摘要
     */
    public static String digest(String password) {
        return digest(password.toCharArray());
    }
    
    /**
     * 生成明文密码的摘要
     * @param password 明文密码
     * @return 返回生成的摘要
     */
    public static String digest(char[] password) {
        return Bytes.toHex(defaultHelper.digest(password));
    }

    public static boolean validate(String password, String digest) {
        return validate(password.toCharArray(), digest);
    }

    public static boolean validate(char[] password, String digest) {
        return defaultHelper.validate(password, Bytes.fromHex(digest));
    }
    
    /**
     * 生成明文密码的base58摘要
     * @param password 明文密码
     * @return 返回生成的摘要
     */
    public static String digestBase58(String password) {
        return digestBase58(password.toCharArray());
    }
    
    /**
     * 生成明文密码的base58摘要
     * @param password 明文密码
     * @return 返回生成的摘要
     */
    public static String digestBase58(char[] password) {
        return Base58.encode(defaultHelper.digest(password));
    }

    public static boolean validateBase58(String password, String digest) {
        return validateBase58(password.toCharArray(), digest);
    }

    public static boolean validateBase58(char[] password, String digest) {
        return defaultHelper.validate(password, Base58.decode(digest));
    }

    public static PasswordDigester digester() {
        return new PasswordHelper();
    }

    public static PasswordValidater validater() {
        return new PasswordHelper();
    }

    /**
     *
     */
    public interface PasswordDigester {

        PasswordDigester saltLength(int saltLength);

        PasswordDigester keyLength(int keyLength);

        PasswordDigester iterationCount(int iterationCount);

        byte[] digest(char[] password);

    }

    /**
     *
     */
    public interface PasswordValidater {

        PasswordValidater saltLength(int saltLength);

        PasswordValidater keyLength(int keyLength);

        PasswordValidater iterationCount(int iterationCount);

        boolean validate(char[] password, byte[] digest);

    }

    /**
     *
     */
    private static class PasswordHelper implements PasswordDigester, PasswordValidater {

        private int saltLength = 8;
        private int keyLength = 8;
        private int iterationCount = 99;

        private PasswordHelper() {}

        @Override
        public PasswordHelper saltLength(int saltLength) {
            this.saltLength = saltLength;
            return this;
        }

        @Override
        public PasswordHelper keyLength(int keyLength) {
            this.keyLength = keyLength;
            return this;
        }

        @Override
        public PasswordHelper iterationCount(int iterationCount) {
            this.iterationCount = iterationCount;
            return this;
        }

        @Override
        public byte[] digest(char[] password) {
            try {
                return pbkdf2(password, saltLength, keyLength, iterationCount);
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }

        @Override
        public boolean validate(char[] password, byte[] digest) {
            byte[] salt = Arrays.copyOfRange(digest, 0, saltLength);
            byte[] hash = Arrays.copyOfRange(digest, saltLength, saltLength + keyLength);
            byte[] testHash = pbkdf2(password, salt, iterationCount, hash.length);
            return slowEquals(hash, testHash);
        }

        /**
         * 将指定的明文密码生成一个加盐PBKDF2哈希值
         *
         * @param password 明文密码
         * @param saltLength 盐字节数
         * @param keyLength 密码字节数
         * @return 返回用指定明文密码生成的一个加盐PBKDF2哈希值
         */
        private byte[] pbkdf2(char[] password, int saltLength, int keyLength, int iterationCount) {
            SecureRandom random = new SecureRandom();
            byte[] salt = new byte[saltLength];
            random.nextBytes(salt);
            byte[] hash = pbkdf2(password, salt, iterationCount, keyLength);
            byte[] bytes = new byte[saltLength + keyLength];
            System.arraycopy(salt, 0, bytes, 0, saltLength);
            System.arraycopy(hash, 0, bytes, saltLength, keyLength);
            return bytes;
        }

        /**
         * Computes the PBKDF2 hash of a password.
         *
         * @param password the password to hash.
         * @param salt the salt
         * @param iterations the iteration count (slowness factor)
         * @param keyLength the length of the hash to compute in bytes
         * @return the PBDKF2 hash of the password
         */
        private byte[] pbkdf2(char[] password, byte[] salt, int iterations, int keyLength) {
            PBEKeySpec spec = new PBEKeySpec(password, salt, iterations, keyLength * 8);
            try {
                SecretKeyFactory skf = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
                return skf.generateSecret(spec).getEncoded();
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }

        /**
         * Compares two byte arrays in length-constant time. This comparison method
         * is used so that password hashes cannot be extracted from an on-line
         * system using a timing attack and then attacked off-line.
         *
         * @param arr1 the first byte array
         * @param arr2 the second byte array
         * @return true if both byte arrays are the same, false if not
         */
        private boolean slowEquals(byte[] arr1, byte[] arr2) {
            int diff = arr1.length ^ arr2.length;
            for (int i = 0; i < arr1.length && i < arr2.length; i++) {
                diff |= arr1[i] ^ arr2[i];
            }
            return diff == 0;
        }

    }
}
